feat(bundler): per-provider tree-shake for vendor chunks - #179
Merged
lukekania merged 2 commits intoMay 18, 2026
Merged
Conversation
Closed
Generalize cross-chunk used-names collection so every chunk gets its own externally-used set, not just main. Vendor chunks holding `@angular/core` or `rxjs` previously pinned every export the package declared because `externally_used = None` made the shaker fall back to entry-walk reachability — on `index.mjs`-style packages that reaches almost everything. - `shake::collect_cross_chunk_used_names_per_provider` returns `Vec<HashSet<String>>` indexed by chunk index; for each chunk i it collects the names other chunks import from any module in i. - `bundle()` builds a bare-specifier → canonical-path map from the existing namespace tables so bare imports (`'@angular/core'`) attribute to the owning vendor chunk, then feeds `externally_used_per_chunk[idx]` into `analyze_unused_exports` for every chunk — the `is_main` gate is dropped. - `npm_wrap::wrap_npm_module` now accepts `unused_exports` and drops both the unused `export const X = ...` declarations and the matching `__exports.X = ns.X` re-export bridges, so shake decisions reach the emitted vendor chunk code. Bumps version to 0.10.13.
lukekania
force-pushed
the
feat/bundler-per-provider-shake
branch
from
May 18, 2026 09:56
da39973 to
05b30d0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the v0.11.0 builder-parity milestone (issue 171 in the tracker stays open until the milestone branch lands).
Summary
shake::collect_cross_chunk_used_names_per_providerreturns aVec<HashSet<String>>indexed by chunk index; for each chunk i it collects the names other chunks import from any module in i. Reuses the existinganalyze_moduleparse + namespace-import expansion paths, adding aspecifier → canonical-pathfallback so bare npm specifiers ('@angular/core') attribute to the owning vendor chunk instead of falling through.bundle()derives that specifier-to-path map from the existingall_file_to_ns/specifier_to_namespacetables, then feedsexternally_used_per_chunk[idx]intoanalyze_unused_exportsfor every chunk — theif is_maingate is gone. Vendor and lazy chunks now get the same cross-chunk consumption signal main has had since PR 131.npm_wrap::wrap_npm_modulenow acceptsunused_exportsand drops both the unusedexport const X = ...declarations and the matching__exports.X = ns.Xre-export bridges, so shake decisions actually reach the emitted vendor chunk code (previously they died at the IIFE wrapper).The old single-provider
collect_cross_chunk_used_namesand itsexpand_namespace_importshelper are removed —_per_providersubsumes them and computing the main-chunk set is justresult[0].Behaviour delta
On the
test-ng-projectfixture (production build), the largest vendor chunk shrinks from ~444 KB to ~416 KB. The gain on real-world@angular/coreis modest because most of its exports are referenced by some module inside the package's own internal graph (so intra-chunk reachability keeps them alive even without entry-walk pinning); the bigger wins were left on the table by the oldexternally_used = Nonefallback when the package shape made the entry walk effectively reach everything. The unit and integration tests verify the mechanism end-to-end on a synthetic package with a true cross-chunk-only export.Tests
shake::tests::test_collect_cross_chunk_used_names_per_provider_multi_chunk— three-chunk graph (main/lazy/vendor), asserts per-provider sets are attributed correctly and unused names don't leak into the wrong chunk.shake::tests::test_collect_cross_chunk_used_names_per_provider_dotted_filename— port of the previous dotted-filename regression onto the new API.npm_wrap::tests::test_wrap_drops_unused_declarationand..._reexport_bridge— unit-level proof thatwrap_npm_modulehonoursunused_exports.vendor_chunk_splitting_integration::vendor_chunk_drops_unreferenced_exports— full pipeline: a vendor chunk whose npm package re-exports two names from animpl.jsdeclaration file emits only the consumed one in its bundled body.Test plan
cargo build --release -p ngc-rscargo test -p ngc-bundler(97 tests pass)cargo clippy --workspace -- -D warningsnpx ng buildagainsttest-ng-projectsucceeds, vendor chunk shrinks measurably